/*
**********************************************************************
*                           The MIT License                          *
*                                                                    *
* Copyright (c) 2011 Flily Hsu                                       *
*                                                                    *
* Permission is hereby granted, free of charge, to any person        *
* obtaining a copy of this software and associated documentation     *
* files (the "Software"), to deal in the Software without            *
* restriction, including without limitation the rights to use, copy, *
* modify, merge, publish, distribute, sublicense, and/or sell copies *
* of the Software, and to permit persons to whom the Software is     *
* furnished to do so, subject to the following conditions:           *
*                                                                    *
* The above copyright notice and this permission notice shall be     *
* included in all copies or substantial portions of the Software.    *
*                                                                    *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,    *
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF *
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND              *
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT        *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,       *
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, *
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER      *
* DEALINGS IN THE SOFTWARE.                                          *
**********************************************************************
*/
#include "brainfuck.h"

#define PC_LABEL "[%4d] "
#ifdef _WIN32
#define LINEBK "\r\n"
#else
#define LINEBK "\n"
#endif

#define CHECK_RETCODE( retcode )			\
	do{										\
		if( BRAINFUCK_SUCCESS != retcode )	\
			return retcode;					\
	}while( 0 )

#define CHECK_NULL_PTR( ptr )							\
	do{													\
		if( NULL == (ptr) )								\
			return BRAINFUCK_ERROR_NULL_POINTER_REF;	\
	}while( 0 )

brainfuck_vm brainfuck_build( void )
{
	int i = 0;
	brainfuck_vm vm = (brainfuck_vm) malloc( sizeof( brainfuck_vminstance) );
	if( NULL == vm )
		return NULL;

	vm->modes = BRAINFUCK_NORMAL_MODE;
	vm->memory = NULL;
	vm->memory_size = 0;
	vm->stack = NULL;
	vm->stack_size = 0;
	vm->ip = 0;
	vm->sp = 0;
	vm->bc = 0;
	vm->pc = 0;
	vm->code = NULL;
	vm->input = stdin;
	vm->output = stdout;
	for( i = 0; i < 256; i++ )
	{
		vm->apis[i].api = brainfuck_default_api;
		vm->apis[i].mode = BRAINFUCK_API_PROTECT_MODE;
	}

	return vm;
}

int brainfuck_setmode( brainfuck_vm vm, int modes )
{
	vm->modes = modes;
	return BRAINFUCK_SUCCESS;
}

int brainfuck_alloc_memory( brainfuck_vm vm, int memory_size )
{
	int size = memory_size * sizeof( bf_byte );
	bf_byte* memory = (bf_byte*) malloc( size );
	if( NULL == memory )
		return BRAINFUCK_ERROR_MEMORY_ALLOC_FAILED;

	memset( memory, 0, size );
	vm->memory = memory;
	vm->memory_size = memory_size;
	return BRAINFUCK_SUCCESS;
}

int brainfuck_alloc_stack( brainfuck_vm vm, int stack_size )
{
	int size = stack_size * sizeof( fpos_t );
	fpos_t* stack = (fpos_t*) malloc( size );
	if( NULL == stack )
		return BRAINFUCK_ERROR_STACK_ALLOC_FAILED;

	memset( stack, 0, size );
	vm->stack = stack;
	vm->stack_size = stack_size;
	return BRAINFUCK_SUCCESS;
}

void brainfuck_free_memory( brainfuck_vm vm )
{
	if( NULL != vm->memory )
	{
		free( vm->memory );
		vm->memory_size = 0;
	}
}

void brainfuck_free_stack( brainfuck_vm vm )
{
	if( NULL != vm->stack )
	{
		free( vm->stack );
		vm->stack_size = 0;
	}
}

int brainfuck_destroy( brainfuck_vm vm, int options )
{
	if( ( options & BRAINFUCK_FREE_MEMORY ) && ( NULL != vm->memory ) )
	{
		free( vm->memory );
		vm->memory = NULL;
		vm->memory_size = 0;
	}

	if( ( options & BRAINFUCK_FREE_STACK ) && ( NULL != vm->stack ) )
	{
		free( vm->stack );
		vm->stack = NULL;
		vm->stack_size = 0;
	}

	if( ( options & BRAINFUCK_FREE_CODE ) && ( NULL != vm->code ) )
	{
		if( 0 != fclose( vm->code ) )
			return BRAINFUCK_ERROR_CLOSE_FILE_FAILED;
		
		vm->code = NULL;
	}

	if( ( options & BRAINFUCK_FREE_INPUT ) &&
		( NULL != vm->input ) && ( vm->input != stdin ) )
	{
		if( 0 != fclose( vm->input ) )
			return BRAINFUCK_ERROR_CLOSE_FILE_FAILED;

		vm->input = NULL;
	}

	if( ( options & BRAINFUCK_FREE_OUTPUT ) &&
		( NULL != vm->output ) && ( vm->output != stdout ) )
	{
		if( 0 != fclose( vm->output ) )
			return BRAINFUCK_ERROR_CLOSE_FILE_FAILED;

		vm->output = NULL;
	}

	free( vm );

	return BRAINFUCK_SUCCESS;
}

int brainfuck_init( brainfuck_vm vm, bf_byte* memory, int memory_size, fpos_t* stack, int stack_size )
{
	int retcode = BRAINFUCK_SUCCESS;

	if( memory )
	{
		vm->memory = memory;
		vm->memory_size = memory_size;
	}
	else
	{
		if( memory_size > 0 )
		{
			retcode = brainfuck_alloc_memory( vm, memory_size );
			CHECK_RETCODE( retcode );
		}
	}

	if( stack )
	{
		vm->stack = stack;
		vm->stack_size = stack_size;
	}
	else
	{
		if( stack_size > 0 )
		{
			retcode = brainfuck_alloc_stack( vm, stack_size );
			CHECK_RETCODE( retcode );
		}
	}

	return BRAINFUCK_SUCCESS;
}

void brainfuck_ioredirect( brainfuck_vm vm, FILE* input, FILE* output )
{
	if( NULL != input )
		vm->input = input;

	if( NULL != output )
		vm->output = output;
}

void brainfuck_load( brainfuck_vm vm, FILE* code )
{
	vm->code = code;
}

int brainfuck_loadfile( brainfuck_vm vm, const char* filename )
{
	FILE* fp = fopen( filename, "rb" );
	if( NULL == fp )
		return BRAINFUCK_ERROR_OPEN_FILE_FAILED;

	vm->code = fp;
	return BRAINFUCK_SUCCESS;
}

int brainfuck_closefile( brainfuck_vm vm )
{
	int retcode;
	if( NULL == vm->code )
		return BRAINFUCK_SUCCESS;

	retcode = fclose( vm->code );
	if( 0 == retcode )
	{
		vm->code = NULL;
		return BRAINFUCK_SUCCESS;
	}
	else
	{
		return BRAINFUCK_ERROR_CLOSE_FILE_FAILED;
	}
}

int brainfuck_runtime_check( brainfuck_vm vm )
{
	fpos_t position;
	int i = 0;
	if( vm->ptr < vm->memory || vm->ptr >= ( vm->memory + vm->memory_size ) )
	{
		fgetpos( vm->code, &position );
		fprintf( vm->output, "\n[Runtime check]Memory overflow." LINEBK );
		fprintf( vm->output, "    PTR = %d, while memory size is %d bytes" LINEBK, vm->ptr - vm->memory, vm->memory_size );
		fprintf( vm->output, "    Offset = %lld( 0x%016llx )" LINEBK, position, position );
		return  BRAINFUCK_ERROR_MEMORY_OVERFLOW;
	}

	if( vm->sp < 0 || vm->sp >= vm->stack_size )
	{
		fgetpos( vm->code, &position );
		fprintf( vm->output, "\n[Runtime check]Loop stack overflow." LINEBK );
		fprintf( vm->output, "    SP = %d, while loop stack size is %d" LINEBK, vm->sp, vm->stack_size );
		fprintf( vm->output, "    Offset = %lld( 0x%016llx )" LINEBK, position, position );
		return BRAINFUCK_ERROR_LOOP_STACK_OVERFLOW;
	}
	
	return BRAINFUCK_SUCCESS;
}

void brainfuck_debug_print( brainfuck_vm vm )
{
	fpos_t position;

	while( NULL != strchr( "+-[],.<>" BRAINFUCK_BREAKPOINT_STR BRAINFUCK_CALLAPI_STR, vm->ip ) )
	{
		if( !( vm->modes & BRAINFUCK_EXTENT_API ) && BRAINFUCK_CALLAPI_CHAR == vm->ip )
			break;

		fgetpos( vm->code, &position );
		fprintf( vm->output, PC_LABEL "INST = '%c', IP = %lld, PTR = %d, [PTR] = %d, LNL = %d" LINEBK,
			vm->pc, vm->ip, position, vm->ptr - vm->memory, *vm->ptr, vm->sp );
		break;
	}
}

int brainfuck_start(  brainfuck_vm vm )
{
	int retcode = BRAINFUCK_SUCCESS;
	/* initialize registers of interpreter */
	vm->ip = 0;
	vm->sp = 0;
	vm->bc = 0;
	vm->pc = 0;
	vm->ptr = vm->memory;

	if( NULL == vm->memory )
	{
		retcode = brainfuck_alloc_memory( vm, DEFAULT_MEMORY_SIZE );
		CHECK_RETCODE( retcode );
	}

	if( NULL == vm->stack )
	{
		retcode = brainfuck_alloc_stack( vm, DEFAULT_STACK_SIZE );
		CHECK_RETCODE( retcode );
	}

	return BRAINFUCK_SUCCESS;
}

int brainfuck_run(  brainfuck_vm vm )
{
	/* load information for interpreter */
	int debug_mode = vm->modes & BRAINFUCK_DEBUG_MODE;
	int check_mode = vm->modes & BRAINFUCK_RUNTIME_CHECK;
	int extend_mode = vm->modes & BRAINFUCK_EXTENT_API;
	int retcode = BRAINFUCK_SUCCESS;

	/* check memory and loop stack */
	if( NULL == vm->memory )
	{
		retcode = brainfuck_alloc_memory( vm, DEFAULT_MEMORY_SIZE );
		CHECK_RETCODE( retcode );
	}

	if( NULL == vm->stack )
	{
		retcode = brainfuck_alloc_stack( vm, DEFAULT_STACK_SIZE );
		CHECK_RETCODE( retcode );
	}

	while( !feof( vm->code ) )
	{
		vm->ip = fgetc( vm->code );
		switch( vm->ip	 )
		{
			case '>':
				++(vm->ptr);
				break;
			case '<':
				--(vm->ptr);
				break;
			case '+':
				++(*vm->ptr);
				break;
			case '-':
				--(*vm->ptr);
				break;
			case '.':
				if( !debug_mode )
				{
					fputc( *vm->ptr, vm->output );
				}
				else
				{
					if( isprint( *vm->ptr ) )
					{
						fprintf( vm->output, PC_LABEL "print '%c'( %d, 0x%02x )" LINEBK,
							vm->pc, *vm->ptr, *vm->ptr, *vm->ptr );
					}
					else
					{
						fprintf( vm->output, PC_LABEL "print ( %d, 0x%02x )" LINEBK,
							vm->pc, *vm->ptr, *vm->ptr );
					}
				}
				break;
			case ',':
				*vm->ptr = (bf_byte) fgetc( vm->input );
				if( debug_mode )
				{
					if( isprint( *vm->ptr ) )
						fprintf( vm->output, PC_LABEL "read a character '%c'( %d, 0x%02x )" LINEBK,
							vm->pc, *vm->ptr, *vm->ptr, *vm->ptr );
					else
						fprintf( vm->output, PC_LABEL "read a byte ( %d, 0x%02x )" LINEBK,
							vm->pc, *vm->ptr, *vm->ptr );
				}
				break;
			case '[':
				if( 0 == *vm->ptr )
				{
					do{
						vm->ip = fgetc( vm->code );
						if( '[' == vm->ip )
							++(vm->bc);

						if( ']' == vm->ip )
						{
							if( 0 == vm->bc ) break;
							else --(vm->bc);
						}
					}while( !feof( vm->code ) );
				}
				else
				{
					if( 0 != fgetpos( vm->code, &vm->stack[vm->sp] ) )
						retcode = BRAINFUCK_ERROR_PUSH_LOOP_STACK;
					++(vm->sp);
				}
				break;
			case ']':
				if( 0 != *vm->ptr )
				{
					if( 0 != fsetpos( vm->code, &vm->stack[vm->sp-1] ) )
						retcode = BRAINFUCK_ERROR_POP_LOOP_STACK;
				}
				else
				{
					--(vm->sp);
				}
				break;
			case BRAINFUCK_BREAKPOINT_CHAR:
				retcode = BRAINFUCK_ERROR_BREAKPOINT_REACH;
				break;
			case BRAINFUCK_CALLAPI_CHAR:
				if( extend_mode )
				{
					bf_byte api_id = *vm->ptr;
					bf_byte* param = vm->ptr + 1;
					int retcode = BRAINFUCK_API_RETURN_NO_VALUE;

					retcode = vm->apis[api_id].api( vm, api_id, param, 0 );
					if( retcode != BRAINFUCK_API_RETURN_NO_VALUE )
						*vm->ptr = (bf_byte) retcode;
				}
				break;
			default:
				break;
		} /* end of switch */

		if( check_mode )
		{
			retcode = brainfuck_runtime_check( vm );
			CHECK_RETCODE( retcode );
		}

		if( debug_mode )
		{
			brainfuck_debug_print( vm );
		}

		++(vm->pc);
		if( BRAINFUCK_SUCCESS != retcode )
			break;
	} /* end of while */

	fflush( vm->output );
	return retcode;
}

int brainfuck_error_message( int errcode, char* errbuf )
{
	switch( errcode )
	{
		case BRAINFUCK_SUCCESS:
			sprintf( errbuf, "Success! No error occurred." );
			break;
		case BRAINFUCK_ERROR_NULL_POINTER_REF:
			sprintf( errbuf, "Reference to null pointer." );
			break;
		case BRAINFUCK_ERROR_STACK_ALLOC_FAILED:
			sprintf( errbuf, "Allocated loop stack failed." );
			break;
		case BRAINFUCK_ERROR_MEMORY_ALLOC_FAILED:
			sprintf( errbuf, "Allocated memory failed." );
			break;
		case BRAINFUCK_ERROR_LOOP_STACK_OVERFLOW:
			sprintf( errbuf, "Loop stack overflow" );
			break;
		case BRAINFUCK_ERROR_MEMORY_OVERFLOW:
			sprintf( errbuf, "Memory overflow" );
			break;
		case BRAINFUCK_ERROR_PUSH_LOOP_STACK:
			sprintf( errbuf, "Push loop stack error" );
			break;
		case BRAINFUCK_ERROR_POP_LOOP_STACK:
			sprintf( errbuf, "Pop loop stack error" );
			break;
		case BRAINFUCK_ERROR_OPEN_FILE_FAILED:
			sprintf( errbuf, "Open file failed." );
			break;
		case BRAINFUCK_ERROR_CLOSE_FILE_FAILED:
			sprintf( errbuf, "Close file failed." );
			break;
		case BRAINFUCK_ERROR_BREAKPOINT_REACH:
			sprintf( errbuf, "Breakpoint reach." );
			break;
		case BRAINFUCK_ERROR_PROTECT_UNLIMITED:
			sprintf( errbuf, "Pass parameter with unlimited length." );
			break;
		default:
			sprintf( errbuf, "Unknown error code" );
			break;
	}

	return 0;
}

int brainfuck_default_api( brainfuck_vm vm, bf_byte callback_id, bf_byte* param, int param_len )
{
	fprintf( vm->output, "API not implemented.\n" );
	return BRAINFUCK_API_RETURN_NO_VALUE;
};

int brainfuck_api_register( brainfuck_vm vm, unsigned char call_id, brainfuck_api api, int param_len, int mode )
{
	if( BRAINFUCK_API_PROTECT_MODE == mode && BRAINFUCK_API_PARAMSIZE_UNLIMITED == param_len)
		return BRAINFUCK_ERROR_PROTECT_UNLIMITED;

	vm->apis[call_id].api = api;
	vm->apis[call_id].param_len = param_len;
	vm->apis[call_id].mode = mode;

	return BRAINFUCK_SUCCESS;
}

int brainfuck_api_call( brainfuck_vm vm, unsigned char call_id )
{
	brainfuck_api_s api = vm->apis[call_id];
	int mode = api.mode;
	bf_byte* param = NULL;
	int retcode = 0;

	if( BRAINFUCK_API_PROTECT_MODE == mode && api.param_len > 0 )
	{
		param = (bf_byte*) malloc( api.param_len );
		memcpy( param, vm->ptr + 1, api.param_len );
		retcode = api.api( vm, call_id, param, api.param_len );
		memcpy( vm->ptr + 1, param, api.param_len );
		free( param );
	}
	else
	{
		if( api.param_len > 0 )
			retcode = api.api( vm, call_id, vm->ptr + 1, api.param_len );
		else
			retcode = api.api( vm, call_id, NULL, api.param_len );
	}

	if( BRAINFUCK_API_RETURN_NO_VALUE != retcode )
		*vm->ptr = (bf_byte) retcode;

	return BRAINFUCK_SUCCESS;
}
